package ro.dta.idbi.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;

import uk.ac.ic.doc.automed.matching.*;
import uk.ac.ic.doc.automed.reps.Schema;

import uk.ac.ic.doc.automed.matching.modules.*;
import uk.ac.ic.doc.automed.matching.modules.bidirectionalmap.*;

/**
 * Relationship descovery using the schema matching component implemented in
 * AutoMed. Code refactored from the implementation of the
 * <i>uk.ac.ic.doc.automed.editorMatchingTool</i> class
 * 
 * @author Tudor Dobrila
 * 
 */
public class RelationshipDiscovery {

	/**
	 * First schema in the schemas pair
	 */
	private final Schema schema1;

	/**
	 * Second schema in the schemas pair
	 */
	private final Schema schema2;

	/**
	 * Tables in the first schema
	 */
	private SchemaElement[] elementsTables1;

	/**
	 * Columns in the first schema
	 */
	private SchemaElement[] elementsColumns1;

	/**
	 * Edges in the first schema
	 */
	private SchemaElement[] elementsColumnEdges1;

	/**
	 * Tables in the second schema
	 */
	private SchemaElement[] elementsTables2;

	/**
	 * Columns in the second schema
	 */
	private SchemaElement[] elementsColumns2;

	/**
	 * Edges in the second schema
	 */
	private SchemaElement[] elementsColumnEdges2;

	private ArrayList<SchemaElementPairRelationship> precisionRels;
	private ArrayList<SchemaElementPairRelationship> numofinstancesRels;
	private ArrayList<SchemaElementPairRelationship> existenceRels;
	private ArrayList<Boolean> nodalModules;
	private ArrayList<SchemaElementPairRelationship[]> nodalModulesResults;
	private ArrayList<Boolean> linknodalAtomicModules;
	private ArrayList<SchemaElementPairRelationship[]> linknodalAtomicModulesResults;
	private ArrayList<Boolean> linknodalNonatomicModules;
	private ArrayList<SchemaElementPairRelationship[]> linknodalNonAtomicModulesResults;
	private ArrayList<String> modules;
	private Boolean trueValue;
	private Boolean falseValue;

	/**
	 * Relationship discovery class constructor
	 * 
	 * @param s1
	 *            First schema in the schemas pair
	 * @param s2
	 *            Second schema in the schemas pair
	 */
	public RelationshipDiscovery(Schema s1, Schema s2) {
		this.schema1 = s1;
		this.schema2 = s2;

		this.precisionRels = new ArrayList<SchemaElementPairRelationship>();
		this.numofinstancesRels = new ArrayList<SchemaElementPairRelationship>();
		this.existenceRels = new ArrayList<SchemaElementPairRelationship>();

		this.modules = new ArrayList<String>();

		this.nodalModules = new ArrayList<Boolean>();
		this.linknodalAtomicModules = new ArrayList<Boolean>();
		this.linknodalNonatomicModules = new ArrayList<Boolean>();
		this.trueValue = new Boolean(true);
		this.falseValue = new Boolean(false);

		this.nodalModulesResults = new ArrayList<SchemaElementPairRelationship[]>();
		this.linknodalAtomicModulesResults = new ArrayList<SchemaElementPairRelationship[]>();
		this.linknodalNonAtomicModulesResults = new ArrayList<SchemaElementPairRelationship[]>();

		try {
			this.elementsTables1 = SchemaElement.getSchemaElements(s1, 1, false);
			this.elementsColumnEdges1 = SchemaElement.getSchemaElements(s1, 2, false);
			this.elementsColumns1 = SchemaElement.getSchemaElements(s1, 2, true);

			this.elementsTables2 = SchemaElement.getSchemaElements(s2, 1, false);
			this.elementsColumnEdges2 = SchemaElement.getSchemaElements(s2, 2, false);
			this.elementsColumns2 = SchemaElement.getSchemaElements(s2, 2, true);
		} catch (Exception e) {
			e.printStackTrace();
			return;
		}

		this.modules.add("Data-Type Module");
		this.modules.add("Statistics Module");
		this.modules.add("Name Module");
		this.modules.add("Number of Instances Module");
		this.modules.add("Existence Module");
		this.modules.add("Naive-Bayes Module");
		this.modules.add("Precision Module");

		this.nodalModules.add(this.falseValue);
		this.nodalModules.add(this.falseValue);
		this.nodalModules.add(this.trueValue);
		this.nodalModules.add(this.trueValue);
		this.nodalModules.add(this.falseValue);
		this.nodalModules.add(this.falseValue);
		this.nodalModules.add(this.falseValue);

		this.linknodalAtomicModules.add(this.falseValue);
		this.linknodalAtomicModules.add(this.falseValue);
		this.linknodalAtomicModules.add(this.trueValue);
		this.linknodalAtomicModules.add(this.trueValue);
		this.linknodalAtomicModules.add(this.trueValue);
		this.linknodalAtomicModules.add(this.falseValue);
		this.linknodalAtomicModules.add(this.trueValue);

		this.linknodalNonatomicModules.add(this.falseValue);
		this.linknodalNonatomicModules.add(this.falseValue);
		this.linknodalNonatomicModules.add(this.trueValue);
		this.linknodalNonatomicModules.add(this.trueValue);
		this.linknodalNonatomicModules.add(this.falseValue);
		this.linknodalNonatomicModules.add(this.falseValue);
		this.linknodalNonatomicModules.add(this.falseValue);
	}

	/**
	 * Discover semantic relationships between elements of the two schemas
	 * 
	 * @return Pairs of semantic relationships between the elements of the two
	 *         schemas
	 */
	public SchemaElementPairRelationship[] discoverRelationships() throws UninstantiatedException,
			RelationshipConflictException, Exception {
		// Compare columns as atoms
		SchemaElementPairRelationship[] discoveredColumnRels = new SchemaElementPairRelationship[0];
		discoveredColumnRels = getRelationshipsColumns(1.1D, 0.85D, 0.3D, 0.15D);

		// Compare columns as link-nodals
		SchemaElementPairRelationship[] discoveredColumnEdgeRels = new SchemaElementPairRelationship[0];
		discoveredColumnEdgeRels = getRelationshipsColumnEdges(1.1D, 0.85D, 0.3D, 0.15D);

		// Compare tables
		SchemaElementPairRelationship[] discoveredTableRels = new SchemaElementPairRelationship[0];
		discoveredTableRels = getRelationshipsTables(1.1D, 0.85D, 0.3D, 0.15D);

		ArrayList<SchemaElementPairRelationship> nonatomic = new ArrayList<SchemaElementPairRelationship>();
		nonatomic.addAll(Arrays.asList(discoveredTableRels));
		nonatomic.addAll(Arrays.asList(discoveredColumnEdgeRels));

		RelationshipExtractor extractor = new SQLRelationshipExtractor();
		extractor.setSchemas(new Schema[] { schema1, schema2 });
		extractor.setAtomicPairRelationships(discoveredColumnRels);
		extractor
				.setNonAtomicPairRelationships((SchemaElementPairRelationship[]) (SchemaElementPairRelationship[]) nonatomic
						.toArray(new SchemaElementPairRelationship[0]));

		return extractor.getRelationships();
	}

	/**
	 * Get relationships between tables
	 */
	private SchemaElementPairRelationship[] getRelationshipsTables(double mainThreshold,
			double eqThreshold, double overThreshold, double disThreshold)
			throws UninstantiatedException, RelationshipConflictException, Exception {
		AbstractModule names = null;
		AbstractModule numOfInstances = null;
		AbstractModule existence = null;
		AbstractModule precision = null;

		if ((this.elementsTables1.length == 0) || (this.elementsTables2.length == 0)) {
			return new SchemaElementPairRelationship[0];
		}

		names = new ElementNameModule(this.elementsTables1, this.elementsTables2);

		numOfInstances = new NumberInstancesModule(this.elementsTables1, this.elementsTables2);

		existence = new CheckExistenceModule(this.elementsTables1, this.elementsTables2);

		precision = new PrecisionModule(this.elementsTables1, this.elementsTables2);

		Filter filter = new Filter(this.elementsTables1, this.elementsTables2);
		filter.addModule(numOfInstances);
		filter.addModule(precision);
		filter.addModule(existence);
		filter.addModule(names);
		SimilarSchemaElementPair[] allpairs = filter.getAllPairs();

		Arrays.sort(allpairs);

		BidirectionalMap map = new BidirectionalMap(eqThreshold, overThreshold, disThreshold);
		DegreeCombinator combinator = new DegreeCombinator(allpairs);
		combinator.setNumberInstancesModule(numOfInstances);
		combinator.setExistenceModule(existence);
		combinator.setPrecisionModule(precision);
		SchemaElementPairRelationship[] rels = combinator.getRelationships(map);

		addPrecisionRels(precision, map);
		addExistenceRels(existence, map);
		addNumofinstancesRels(numOfInstances, map);

		this.nodalModulesResults.add(null);
		this.nodalModulesResults.add(null);

		this.nodalModulesResults.add(getRelationships(names, map));

		this.nodalModulesResults.add(getRelationships(numOfInstances, map));

		this.nodalModulesResults.add(getRelationships(existence, map));
		this.nodalModulesResults.add(null);
		this.nodalModulesResults.add(null);

		this.nodalModulesResults.add(getRelationships(precision, map));

		return rels;
	}

	/**
	 * Get relationships between columns
	 */
	private SchemaElementPairRelationship[] getRelationshipsColumns(double mainThreshold,
			double eqThreshold, double overThreshold, double disThreshold)
			throws UninstantiatedException, RelationshipConflictException, Exception {
		if ((this.elementsColumns1.length == 0) || (this.elementsColumns2.length == 0)) {
			return new SchemaElementPairRelationship[0];
		}

		AbstractModule datatype = null;
		AbstractModule statistics = null;
		AbstractModule names = null;
		AbstractModule numOfInstances = null;
		AbstractModule existence = null;
		AbstractModule naivebayes = null;
		AbstractModule precision = null;

		datatype = new DataTypeModule(this.elementsColumns1, this.elementsColumns2);
		datatype.getBidirectionalDegrees();

		statistics = new StatisticsModule(this.elementsColumns1, this.elementsColumns2);
		statistics.getBidirectionalDegrees();

		names = new ElementNameModule(this.elementsColumns1, this.elementsColumns2);
		names.getBidirectionalDegrees();

		numOfInstances = new NumberInstancesModule(this.elementsColumns1, this.elementsColumns2);
		numOfInstances.getBidirectionalDegrees();

		existence = new CheckExistenceModule(this.elementsColumns1, this.elementsColumns2);
		existence.getBidirectionalDegrees();

		precision = new PrecisionModule(this.elementsColumns1, this.elementsColumns2);
		precision.getBidirectionalDegrees();

		naivebayes = new NaiveBayesModule(this.elementsColumns1, this.elementsColumns2);
		naivebayes.getBidirectionalDegrees();

		Filter filter = new Filter(this.elementsColumns1, this.elementsColumns2);
		filter.addModule(datatype);
		filter.addModule(statistics);
		filter.addModule(naivebayes);
		filter.addModule(precision);
		filter.addModule(names);
		filter.addModule(existence);
		SimilarSchemaElementPair[] filtered = filter.getCompatibles(mainThreshold);

		Aggregator aggregator = new Aggregator(filtered);
		aggregator.addNonAuxiliaryModule(datatype);
		aggregator.addNonAuxiliaryModule(statistics);
		aggregator.addNonAuxiliaryModule(precision);
		aggregator.addNonAuxiliaryModule(existence);
		aggregator.addNonAuxiliaryModule(numOfInstances);
		aggregator.addAuxiliaryModule(names);
		SimilarSchemaElementPair[] aggregated = aggregator.getPairs();

		Arrays.sort(aggregated);

		BidirectionalMap map = new BidirectionalMap(eqThreshold, overThreshold, disThreshold);
		DegreeCombinator combinator = new DegreeCombinator(aggregated);
		combinator.setNumberInstancesModule(numOfInstances);
		combinator.setExistenceModule(existence);
		combinator.setPrecisionModule(precision);
		SchemaElementPairRelationship[] rels = combinator.getRelationships(map);

		for (Iterator<Boolean> it = this.linknodalAtomicModules.iterator(); it.hasNext();) {
			if (it.next().booleanValue()) {
				this.linknodalAtomicModulesResults.add(getRelationships(datatype, map));
				this.linknodalAtomicModulesResults.add(getRelationships(statistics, map));
				this.linknodalAtomicModulesResults.add(getRelationships(names, map));
				this.linknodalAtomicModulesResults.add(getRelationships(numOfInstances, map));
				this.linknodalAtomicModulesResults.add(getRelationships(existence, map));
				this.linknodalAtomicModulesResults.add(getRelationships(naivebayes, map));
				this.linknodalAtomicModulesResults.add(getRelationships(precision, map));
			}
		}

		addPrecisionRels(precision, map);
		addExistenceRels(existence, map);
		addNumofinstancesRels(numOfInstances, map);

		return rels;
	}

	/**
	 * Get relationships between edges
	 */
	private SchemaElementPairRelationship[] getRelationshipsColumnEdges(double mainThreshold,
			double eqThreshold, double overThreshold, double disThreshold)
			throws UninstantiatedException, RelationshipConflictException, Exception {
		if ((this.elementsColumnEdges1.length == 0) || (this.elementsColumnEdges2.length == 0)) {
			return new SchemaElementPairRelationship[0];
		}

		AbstractModule names = null;
		AbstractModule numOfInstances = null;
		AbstractModule existence = null;
		AbstractModule precision = null;

		names = new ElementNameModule(this.elementsColumnEdges1, this.elementsColumnEdges2);

		numOfInstances = new NumberInstancesModule(this.elementsColumnEdges1,
				this.elementsColumnEdges2);

		existence = new CheckExistenceModule(this.elementsColumnEdges1, this.elementsColumnEdges2);

		precision = new PrecisionModule(this.elementsColumnEdges1, this.elementsColumnEdges2);

		Filter filter = new Filter(this.elementsColumnEdges1, this.elementsColumnEdges2);

		filter.addModule(precision);
		filter.addModule(names);
		filter.addModule(existence);
		filter.addModule(numOfInstances);
		SimilarSchemaElementPair[] allpairs = filter.getAllPairs();

		Arrays.sort(allpairs);

		BidirectionalMap map = new BidirectionalMap(eqThreshold, overThreshold, disThreshold);
		DegreeCombinator combinator = new DegreeCombinator(allpairs);
		combinator.setNumberInstancesModule(numOfInstances);
		combinator.setExistenceModule(existence);
		combinator.setPrecisionModule(precision);
		SchemaElementPairRelationship[] rels = combinator.getRelationships(map);

		this.linknodalNonAtomicModulesResults.add(null);
		this.linknodalNonAtomicModulesResults.add(null);
		this.linknodalNonAtomicModulesResults.add(getRelationships(names, map));
		this.linknodalNonAtomicModulesResults.add(getRelationships(numOfInstances, map));
		this.linknodalNonAtomicModulesResults.add(getRelationships(existence, map));
		this.linknodalNonAtomicModulesResults.add(null);
		this.linknodalNonAtomicModulesResults.add(null);
		this.linknodalNonAtomicModulesResults.add(getRelationships(precision, map));

		return rels;
	}

	private SchemaElementPairRelationship[] getRelationships(AbstractModule module,
			BidirectionalMap map) {
		SimilarSchemaElementPair[] pairs = module.getBidirectionalDegrees();
		SchemaElementPairRelationship[] result = new SchemaElementPairRelationship[pairs.length];
		for (int i = 0; i < pairs.length; ++i) {
			SchemaElement first = pairs[i].getFirstElement();
			SchemaElement second = pairs[i].getSecondElement();
			BidirectionalDegree degree = pairs[i].getDegree();
			result[i] = new SchemaElementPairRelationship(first, second,
					map.getRelationship(degree));
		}

		return result;
	}

	private void addPrecisionRels(AbstractModule module, BidirectionalMap map) {
		addRels(module.getBidirectionalDegrees(), map, this.precisionRels);
	}

	private void addExistenceRels(AbstractModule module, BidirectionalMap map) {
		addRels(module.getBidirectionalDegrees(), map, this.existenceRels);
	}

	private void addNumofinstancesRels(AbstractModule module, BidirectionalMap map) {
		addRels(module.getBidirectionalDegrees(), map, this.numofinstancesRels);
	}

	private void addRels(SimilarSchemaElementPair[] pairs, BidirectionalMap map,
			ArrayList<SchemaElementPairRelationship> rels) {
		for (int i = 0; i < pairs.length; ++i) {
			SchemaElement first = pairs[i].getFirstElement();
			SchemaElement second = pairs[i].getSecondElement();
			BidirectionalDegree degree = pairs[i].getDegree();
			rels.add(new SchemaElementPairRelationship(first, second, map.getRelationship(degree)));
		}
	}
}
